package no.sachmuld.methodcombination.api;

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 *
 */
public abstract class SimpleTestTemplate {

    @Test
    public void shouldExecuteBehaviourOrMethodStyle() throws Exception {

        Foo foo = createFoo();
        foo.with(new BooleanBehaviour(false));
        foo.with(new BooleanBehaviour(true));
        foo.with(new NegativeBooleanBehaviour());

        boolean expected = false;
        boolean actual = foo.or();

        Assert.assertTrue(expected == actual);

    }

    @Test
    public void shouldExecuteBehaviourAndMethodStyle() throws Exception {

        Foo foo = createFoo();
        foo.with(new BooleanBehaviour(false));
        foo.with(new BooleanBehaviour(true));
        foo.with(new NegativeBooleanBehaviour());

        boolean expected = true;
        boolean actual = foo.and();

        Assert.assertTrue(expected == actual);

    }

    @Test
    public void shouldExecuteBehaviourMinMethodStyle() throws Exception {

        Foo foo = createFoo();
        foo.with(new IntBehaviour(1));
        foo.with(new IntBehaviour(2));
        foo.with(new NegativeIntBehaviour());

        int expected = -1;
        int actual = foo.min();

        Assert.assertEquals(expected, actual);

    }

    @Test
    public void shouldExecuteBehaviourMaxMethodStyle() throws Exception {

        Foo foo = createFoo();
        foo.with(new IntBehaviour(1));
        foo.with(new IntBehaviour(2));
        foo.with(new NegativeIntBehaviour());

        int expected = -2;
        int actual = foo.max();

        Assert.assertEquals(expected, actual);

    }

    @Test
    public void shouldExecuteBehaviourSumMethodStyle() throws Exception {

        Foo foo = createFoo();
        foo.with(new IntBehaviour(1));
        foo.with(new IntBehaviour(2));
        foo.with(new NegativeIntBehaviour());

        int expected = -3;
        int actual = foo.sum();

        Assert.assertEquals(expected, actual);

    }

    @Test
    public void shouldExecuteBehaviourAllMethodStyle() throws Exception {
        List<Step> expected = Arrays.asList(Step.AROUND_Y_BEGIN, Step.AROUND_X_BEGIN, Step.Y, Step.X, Step.AROUND_X_END, Step.AROUND_Y_END);
        final List<Step> actual = new ArrayList<>();
        Foo foo = createFoo();

        foo.with(new AroundFooBehaviour() {
            @Override
            public void aroundMethod(MethodInvocation<Void> methodInvocation) {
                actual.add(Step.AROUND_X_BEGIN);
                try {
                    methodInvocation.proceed();
                } finally {
                    actual.add(Step.AROUND_X_END);
                }
            }
        });
        foo.with(new PrimaryFooBehaviour() {

            @Override
            public void method() {
                actual.add(Step.X);
            }
        });
        foo.with(new AroundFooBehaviour() {
            @Override
            public void aroundMethod(MethodInvocation<Void> methodInvocation) {
                actual.add(Step.AROUND_Y_BEGIN);
                try {
                    methodInvocation.proceed();
                } finally {
                    actual.add(Step.AROUND_Y_END);
                }
            }
        });
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public void method() {
                actual.add(Step.Y);
            }
        });

        foo.all();

        Assert.assertEquals(expected, actual);

    }

    @Test
    public void shouldExecuteBehaviourListMethodStyle() throws Exception {

        Foo foo = createFoo();

        foo.with(new AroundFooBehaviour() {
            @Override
            public List<Integer> aroundListValue(MethodInvocation<List<Integer>> methodInvocation) {
                List<Integer> list = methodInvocation.proceed();
                list.add(0);
                return list;
            }
        });
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public int listValue() {
                return 1;
            }
        });
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public int listValue() {
                return 2;
            }
        });

        List<Integer> expected = Arrays.asList(2, 1, 0);
        List<Integer> actual = foo.list();

        Assert.assertEquals(expected, actual);

    }

    @Test
    public void shouldExecuteBehaviourAppendMethodStyle() throws Exception {

        Foo foo = createFoo();

        foo.with(new AroundFooBehaviour() {
            @Override
            public List<Integer> aroundAppendValue(MethodInvocation<List<Integer>> methodInvocation) {
                List<Integer> list = methodInvocation.proceed();
                list.add(0);
                return list;
            }
        });
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public List<Integer> appendValue() {
                return Arrays.asList(2, 1);
            }
        });
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public List<Integer> appendValue() {
                return Arrays.asList(4, 3);
            }
        });

        List<Integer> expected = Arrays.asList(4, 3, 2, 1, 0);
        List<Integer> actual = foo.append();

        Assert.assertEquals(expected, actual);

    }

    @Test
    public void shouldExecutePrimaryMethodsTopDown() throws Exception {
        List<Step> expected = Arrays.asList(Step.X, Step.Y);
        final List<Step> actual = new ArrayList<>();

        Foo foo = createFoo();
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public void method() {
                actual.add(Step.X);
            }
        });
        foo.with(new PrimaryFooBehaviour() {
            @Override
            public void method() {
                actual.add(Step.Y);
            }
        });

        foo.allInReverse();

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void shouldAcceptMissingAroundMethod() throws Exception {
        List<Step> expected = Arrays.asList(Step.X);
        final List<Step> actual = new ArrayList<>();

        FooWithNoAroundMethod foo = createFooWithNoAroundMethod();
        foo.with(new FooBehaviourWithNoAroundMethod() {
            @Override
            public void method() {
                actual.add(Step.X);
            }
        });

        foo.all();

        Assert.assertEquals(expected, actual);

    }

    static public enum Step {
        AROUND_X_BEGIN, AROUND_X_END, X, AROUND_Y_BEGIN, AROUND_Y_END, Y
    }

    static public interface FooBehaviour {}

    static public abstract class AroundFooBehaviour implements FooBehaviour {

        public boolean aroundBooleanValue(MethodInvocation<Boolean> methodInvocation) {
            return methodInvocation.proceed();
        }
        public int aroundIntValue(MethodInvocation<Integer> methodInvocation) {
            return methodInvocation.proceed();
        }
        public void aroundMethod(MethodInvocation<Void> methodInvocation) {
            methodInvocation.proceed();
        }
        public List<Integer> aroundListValue(MethodInvocation<List<Integer>> methodInvocation) {
            return methodInvocation.proceed();
        }
        public List<Integer> aroundAppendValue(MethodInvocation<List<Integer>> methodInvocation) {
            return methodInvocation.proceed();
        }
    }

    static public class NotImplementedException extends RuntimeException {}

    static public abstract class PrimaryFooBehaviour implements FooBehaviour {

        public boolean booleanValue() {
            throw new NotImplementedException();
        }
        public int intValue() {
            throw new NotImplementedException();
        }
        public void method() {
            throw new NotImplementedException();
        }
        public int listValue() {
            throw new NotImplementedException();
        }
        public List<Integer> appendValue() {
            throw new NotImplementedException();
        }

    }


    static public abstract class Foo extends MethodCombinationSupport<FooBehaviour> {
        public abstract boolean or();
        public abstract boolean and();
        public abstract int min();
        public abstract int max();
        public abstract int sum();
        public abstract void all();
        public abstract void allInReverse();
        public abstract List<Integer> list();
        public abstract List<Integer> append();
    }

    static abstract public class FooBehaviourWithNoAroundMethod {
        public abstract void method();
    }

    static public abstract class FooWithNoAroundMethod extends MethodCombinationSupport<FooBehaviourWithNoAroundMethod> {
        public abstract void all();
    }

    static class BooleanBehaviour extends PrimaryFooBehaviour {
        private boolean b;

        BooleanBehaviour(boolean b) {
            this.b = b;
        }

        @Override
        public boolean booleanValue() {
            return b;
        }
    }

    static class NegativeBooleanBehaviour extends AroundFooBehaviour {
        @Override
        public boolean aroundBooleanValue(MethodInvocation<Boolean> methodInvocation) {
            return !methodInvocation.proceed();
        }
    }

    static class IntBehaviour extends PrimaryFooBehaviour {
        private int i;

        IntBehaviour(int i) {
            this.i = i;
        }
        @Override
        public int intValue() {
            return i;
        }
    }

    static class NegativeIntBehaviour extends AroundFooBehaviour {
        @Override
        public int aroundIntValue(MethodInvocation<Integer> methodInvocation) {
            return -methodInvocation.proceed();
        }
    }

    protected abstract Foo createFoo();
    protected abstract FooWithNoAroundMethod createFooWithNoAroundMethod();
}
